/*
 * Copyright 2016 requery.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.requery.ohos;

import io.requery.meta.EntityModel;
import io.requery.meta.Type;
import io.requery.proxy.EntityProxy;
import io.requery.query.Result;
import io.requery.sql.EntityDataStore;
import io.requery.sql.ResultSetIterator;
import io.requery.util.function.Function;
import ohos.agp.components.BaseItemProvider;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.data.resultset.ResultSet;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;

import java.io.Closeable;
import java.sql.SQLException;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * An implementation of {@link BaseItemProvider} specifically for displaying items from a
 * {@link EntityDataStore} query. To use extend this class and implement {@link #performQuery()}
 * and {@link #getComponent(int, Component, ComponentContainer)}}.
 *
 * @param <E> entity element type
 *
 * @author Nikhil Purushe
 */
public abstract class QueryAdapter<E> extends BaseItemProvider implements Closeable {

    private final EventHandler handler;
    private final Function<E, EntityProxy<E>> proxyProvider;
    private ResultSetIterator<E> iterator;
    private boolean createdExecutor;
    private ExecutorService executor;
    private Future<Result<E>> queryFuture;

    /**
     * Creates a new adapter instance.
     *
     * @param model database entity model
     * @param type entity class type
     */
    protected QueryAdapter(EntityModel model, Class<E> type) {
        this(model.typeOf(type));
    }

    /**
     * Creates a new adapter instance without any type mapping.
     */
    protected QueryAdapter() {
        this(null);
    }

    /**
     * Creates a new adapter instance mapped to the given type.
     *
     * @param type entity type
     */
    protected QueryAdapter(Type<E> type) {
        proxyProvider = type == null ? null : type.getProxyProvider();
        handler = new EventHandler(EventRunner.getMainEventRunner());
    }

    /**
     * Call this to clean up the underlying result.
     */
    @Override
    public void close() {
        if (queryFuture != null) {
            queryFuture.cancel(true);
        }
        if (iterator != null) {
            iterator.close();
            iterator = null;
        }
    }

    /**
     * Sets the {@link Executor} used for running the query off the main thread.
     *
     * @param executor ExecutorService to use for the background query.
     */
    public void setExecutor(ExecutorService executor) {
        if (createdExecutor && this.executor != null) {
            this.executor.shutdown();
        }
        this.executor = executor;
    }

    /**
     * Sets the results the adapter should display.
     *
     * @param iterator result set iterator
     */
    public void setResult(ResultSetIterator<E> iterator) {
        close();
        this.iterator = iterator;
        notifyDataChanged();
    }

    /**
     * Schedules the query to be run in the background. After completion
     * {@link #setResult(ResultSetIterator)} will be called to update the contents of the adapter.
     * Note if an {@link Executor} was not specified one will be created automatically to run the
     * query.
     */
    public void queryAsync() {
        if (this.executor == null) {
            this.executor = Executors.newSingleThreadExecutor();
            createdExecutor = true;
        }
        if (queryFuture != null && !queryFuture.isDone()) {
            queryFuture.cancel(true);
        }
        queryFuture = executor.submit(new Callable<Result<E>>() {
            @Override
            public Result<E> call() {
                final Result<E> result = performQuery();

                handler.postTask(new Runnable() {
                    @Override
                    public void run() {
                        setResult((ResultSetIterator<E>) result.iterator());
                    }
                });
                return result;
            }
        });
    }

    /**
     * Implement this method with your query operation. Note this method is executed on a
     * background thread not the main thread.
     *
     * @see #setExecutor(ExecutorService)
     *
     * @return query result set
     */
    public abstract Result<E> performQuery();

    @Override
    public E getItem(int position) {
        if (iterator == null) {

            return null;
        }
        return iterator.get(position);
    }

    @Override
    public Component getComponent(int position, Component convertView, ComponentContainer parent) {
        E item = getItem(position);
        return getComponent( item, convertView, parent);
    }


    /**
     * Called to display the data at the specified position for the given item. The item is
     * retrieved from the result iterator.
     *
     * @param item entity element to bind to the view
     * @param convertView view being recycled
     * @param parent parent view
     * @return component
     */
    public abstract Component getComponent(E item, Component convertView, ComponentContainer parent);

    public abstract int getPersonCount();

    @Override
    public long getItemId(int position) {
        E item = iterator.get(position);
        Object key = null;
        if (proxyProvider != null) {
            EntityProxy<E> proxy = proxyProvider.apply(item);
            key = proxy.key();
        }
        return key == null ? item.hashCode() : key.hashCode();
    }

    @Override
    public int getCount() {
        if (iterator == null) {
            int count = 0;
            count = getPersonCount();
            return count;
        }
        try {
            ResultSet cursor = iterator.unwrap(ResultSet.class);
            return cursor.getRowCount();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}
